package com.jorado.apollo;

import com.ctrip.framework.apollo.openapi.client.ApolloOpenApiClient;
import com.ctrip.framework.apollo.openapi.client.constant.ApolloOpenApiConstants;
import com.ctrip.framework.apollo.openapi.dto.*;
import com.ctrip.framework.apollo.util.ExceptionUtil;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.jorado.apollo.model.Result;
import com.jorado.apollo.model.ResultStatus;
import com.jorado.apollo.util.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.List;

/**
 * 阿波罗openapi客户端代理
 */
public class ApolloClientProxy {
    private static final Logger LOGGER = LoggerFactory.getLogger(ApolloClientProxy.class);
    private ApolloOpenApiClient client;
    private ApolloClientOptions clientOptions;

    private ApolloClientProxy(ApolloOpenApiClient apolloOpenApiClient, ApolloClientOptions clientOptions) {
        this.clientOptions = clientOptions;
        this.client = apolloOpenApiClient;
    }

    public static ApolloClientBuilder newBuilder(String portalUrl, String token, String userName) {
        return new ApolloClientBuilder(portalUrl, token, userName);
    }

    /**
     * 获取集群信息
     *
     * @param appId
     * @return
     */
    public Result<List<OpenEnvClusterDTO>> getClusterInfo(String appId) {
        try {
            List<OpenEnvClusterDTO> dto = this.client.getEnvClusterInfo(appId);
            return Result.ofSuccess(dto);
        } catch (Exception ex) {
            LOGGER.error(String.format("get env cluster info error,appId:%s,option:%s", appId, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 创建公共配置命名空间
     *
     * @param namespace
     * @return
     */
    public Result createNamespace(String namespace) {
        return createNamespace(namespace, false);
    }

    /**
     * 创建配置命名空间
     *
     * @param namespace
     * @param isPublic
     * @return
     */
    public Result createNamespace(String namespace, boolean isPublic) {
        try {
            Result result = getNamespace(namespace);
            if (result.isFail()) {
                OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO();
                dto.setAppId(this.clientOptions.getAppId());
                dto.setName(namespace);
                dto.setPublic(isPublic);
                dto.setDataChangeCreatedBy(this.clientOptions.getUserName());
                dto.setFormat("properties");
                dto.setComment("create by [" + this.clientOptions.getUserName() + "] at:" + DateUtils.timeNow());
                this.client.createAppNamespace(dto);
            }
            return Result.OK;
        } catch (Exception ex) {
            LOGGER.error(String.format("create namespace error,namespace:%s,ispublic:%s,option:%s", namespace, isPublic, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 发布配置
     *
     * @param namespace
     * @param title
     * @return
     */
    public Result publishConfig(String namespace, String title) {
        try {
            NamespaceReleaseDTO dto = new NamespaceReleaseDTO();
            dto.setReleaseTitle(title);
            dto.setReleaseComment("publish by [" + this.clientOptions.getUserName() + "] at:" + DateUtils.timeNow());
            dto.setReleasedBy(this.clientOptions.getUserName());
            this.client.publishNamespace(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace, dto);
            return Result.OK;
        } catch (Exception ex) {
            LOGGER.error(String.format("public config error,namespace:%s,title:%s,option:%s", namespace, title, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 获取最后生效的配置
     *
     * @param namespace
     * @return
     */
    public Result<OpenReleaseDTO> getLastConfig(String namespace) {
        try {
            OpenReleaseDTO dto = this.client.getLatestActiveRelease(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace);
            return Result.ofSuccess(dto);
        } catch (Exception ex) {
            LOGGER.error(String.format("get last listener error,namespace:%s,option:%s", namespace, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 删除配置
     *
     * @param namespace
     * @param keys
     * @return
     */
    public Result removeConfig(String namespace, String... keys) {
        try {
            for (String key : keys) {
                this.client.removeItem(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace, key, this.clientOptions.getUserName());
            }
            return Result.OK;
        } catch (Exception ex) {
            LOGGER.error(String.format("remove config error,namespace:%s,keys:%s,option:%s", namespace, keys, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 删除配置
     *
     * @param namespace
     * @param keys
     * @return
     */
    public Result removeConfig(String namespace, Collection<String> keys) {
        String[] array = new String[keys.size()];
        return removeConfig(namespace, keys.toArray(array));
    }

    /**
     * 删除应用下的配置
     *
     * @param namespace
     * @return
     */
    public Result removeAllConfig(String namespace) {
        Result<List<OpenItemDTO>> result = getConfigs(namespace);
        if (result.isFail()) {
            return result;
        }
        result.getData().forEach(n -> this.removeConfig(namespace, n.getKey()));
        return Result.OK;
    }

    /**
     * 删除命名空间下的所有配置节点
     *
     * @param namespace
     * @return
     */
    public Result clearConfig(String namespace) {
        try {
            Result<List<OpenItemDTO>> result = this.getConfigs(namespace);
            if (result.isFail()) {
                return result;
            }
            result.getData().forEach(n -> {
                this.client.removeItem(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace, n.getKey(), this.clientOptions.getUserName());
            });
            return Result.OK;
        } catch (Exception ex) {
            LOGGER.error(String.format("clear config error,namespace:%s,option:%s", namespace, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 老接口
     *
     * @param namespace
     * @param key
     * @return
     */
    public Result<OpenItemDTO> getConfigFromNamespace(String namespace, String key) {
        try {

            Result<List<OpenItemDTO>> result = getConfigs(namespace);
            if (result.isFail()) {
                return Result.ofFail(result.getMessage());
            }

            for (OpenItemDTO item : result.getData()) {
                if (item.getKey().equals(key)) {
                    return Result.ofSuccess(item);
                }
            }
            return Result.NOT_FOUND;
        } catch (Exception ex) {
            LOGGER.error(String.format("get config error,namespace:%s,key:%s,option:%s", namespace, key, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 获取配置
     *
     * @param namespace
     * @param key
     * @return
     */
    public Result<OpenItemDTO> getConfig(String namespace, String key) {
        try {
            OpenItemDTO dto = this.client.getItem(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace, key);
            return Result.ofSuccess(dto);
        } catch (Exception ex) {
            LOGGER.error(String.format("get config error,namespace:%s,key:%s,option:%s", namespace, key, getClientOptions().toString()), ex);
            return getConfigFromNamespace(namespace, key);
        }
    }

    /**
     * 获取命名空间数据
     *
     * @param namespace
     * @return
     */
    public Result<OpenNamespaceDTO> getNamespace(String namespace) {
        try {
            OpenNamespaceDTO namespaceDTO = this.client.getNamespace(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace);
            return Result.ofSuccess(namespaceDTO);
        } catch (Exception ex) {
            LOGGER.error(String.format("get namespace error,namespace:%s,option:%s", namespace, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 获取所有命名空间数据
     *
     * @return
     */
    public Result<List<OpenItemDTO>> getConfigs(String namespace) {

        Result<OpenNamespaceDTO> result = getNamespace(namespace);
        if (result.isFail()) {
            return Result.ofFail(result.getMessage());
        }
        return new Result(result.getData().getItems());
    }

    /**
     * 获取所有命名空间数据
     *
     * @return
     */
    public Result<List<OpenNamespaceDTO>> getAllNamespace() {
        try {
            List<OpenNamespaceDTO> namespaceDTOList = this.client.getNamespaces(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName());
            return Result.ofSuccess(namespaceDTOList);
        } catch (Exception ex) {
            LOGGER.error(String.format("get all namespace error,option:%s", getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 新增或者修改配置
     *
     * @param namespace
     * @param key
     * @param value
     * @return
     */
    public Result createOrUpdateConfig(String namespace, String key, String value) {
        return createOrUpdateConfig(namespace, key, value, DateUtils.timeNow());
    }

    /**
     * 新增或者修改配置
     *
     * @param namespace
     * @param key
     * @param value
     * @return
     */
    public Result createOrUpdateConfig(String namespace, String key, String value, String comment) {
        try {
            Result result = getConfig(namespace, key);
            if (result.isFail()) {
                return this.createConfig(namespace, key, value, comment);
            } else {
                return this.updateConfig(namespace, key, value, comment);
            }
        } catch (Exception ex) {
            LOGGER.error(String.format("create or update config error,namespace:%s,key:%s,value:%s,option:%s", namespace, key, value, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 新增配置
     *
     * @param namespace
     * @param key
     * @param value
     * @return
     */
    public Result createConfig(String namespace, String key, String value, String comment) {
        try {
            OpenItemDTO dto = new OpenItemDTO();
            dto.setKey(key);
            dto.setValue(value);
            dto.setComment(comment);
            dto.setDataChangeCreatedBy(this.clientOptions.getUserName());
            this.client.createItem(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace, dto);
            return Result.OK;
        } catch (Exception ex) {
            LOGGER.error(String.format("create config error,namespace:%s,key:%s,value:%s,option:%s", namespace, key, value, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 新增配置
     *
     * @param namespace
     * @param key
     * @param value
     * @return
     */
    public Result createConfig(String namespace, String key, String value) {
        return createConfig(namespace, key, value, DateUtils.timeNow());
    }

    /**
     * 修改配置
     *
     * @param namespace
     * @param key
     * @param value
     * @return
     */
    public Result updateConfig(String namespace, String key, String value, String comment) {
        try {
            OpenItemDTO dto = new OpenItemDTO();
            dto.setKey(key);
            dto.setValue(value);
            dto.setComment(comment);
            dto.setDataChangeCreatedBy(this.clientOptions.getUserName());
            dto.setDataChangeLastModifiedBy(this.clientOptions.getUserName());
            this.client.updateItem(this.clientOptions.getAppId(), this.clientOptions.getEnv(), this.clientOptions.getClusterName(), namespace, dto);
            return Result.OK;
        } catch (Exception ex) {
            LOGGER.error(String.format("update config error,namespace:%s,key:%s,value:%s,option:%s", namespace, key, value, getClientOptions().toString()), ex);
            return errorResult(ex);
        }
    }

    /**
     * 修改配置
     *
     * @param namespace
     * @param key
     * @param value
     * @return
     */
    public Result updateConfig(String namespace, String key, String value) {
        return updateConfig(namespace, key, value, DateUtils.timeNow());
    }

    private Result errorResult(String... args) {
        StringBuilder messageBuilder = new StringBuilder();
        messageBuilder.append("option:" + getClientOptions().toString());
        for (String s : args) {
            messageBuilder.append(",");
            messageBuilder.append(s);
        }
        return Result.ofFail(ResultStatus.CONFIG_ERROR, messageBuilder.toString());
    }

    private Result errorResult(Throwable ex) {
        return errorResult(ExceptionUtil.getDetailMessage(ex));
    }

    public ApolloClientOptions getClientOptions() {
        return clientOptions;
    }

    public ApolloOpenApiClient getClient() {
        return client;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"clientOptions\":")
                .append(clientOptions);
        sb.append('}');
        return sb.toString();
    }

    public static class ApolloClientBuilder {
        private String portalUrl;
        private String token;
        private String userName;

        private int connectTimeout = -1;
        private int readTimeout = -1;

        private String appId;
        private String env = "dev";
        private String clusterName = "default";

        public ApolloClientBuilder(String portalUrl, String token, String userName) {
            this.portalUrl = portalUrl;
            this.token = token;
            this.userName = userName;
        }

        public ApolloClientBuilder withConnectTimeout(int connectTimeout) {
            this.connectTimeout = connectTimeout;
            return this;
        }

        public ApolloClientBuilder withReadTimeout(int readTimeout) {
            this.readTimeout = readTimeout;
            return this;
        }

        public ApolloClientBuilder withAppId(String appId) {
            this.appId = appId;
            return this;
        }

        public ApolloClientBuilder withEnv(String env) {
            this.env = env;
            return this;
        }

        public ApolloClientBuilder withClusterName(String clusterName) {
            this.clusterName = clusterName;
            return this;
        }

        public ApolloClientProxy build() {

            Preconditions.checkArgument(!Strings.isNullOrEmpty(appId), "appid should not be null or empty!");
            Preconditions.checkArgument(!Strings.isNullOrEmpty(env), "env should not be null or empty!");
            Preconditions.checkArgument(!Strings.isNullOrEmpty(clusterName), "cluster name should not be null or empty!");
            Preconditions.checkArgument(!Strings.isNullOrEmpty(userName), "user name should not be null or empty!");

            if (connectTimeout < 0) {
                connectTimeout = ApolloOpenApiConstants.DEFAULT_CONNECT_TIMEOUT;
            }

            if (readTimeout < 0) {
                readTimeout = ApolloOpenApiConstants.DEFAULT_READ_TIMEOUT;
            }

            ApolloOpenApiClient apolloOpenApiClient = ApolloOpenApiClient.newBuilder()
                    .withPortalUrl(portalUrl)
                    .withToken(token)
                    .withConnectTimeout(connectTimeout)
                    .withReadTimeout(readTimeout)
                    .build();

            ApolloClientOptions clientOptions = new ApolloClientOptions();
            clientOptions.setPortalUrl(portalUrl);
            clientOptions.setToken(token);
            clientOptions.setAppId(appId);
            clientOptions.setClusterName(clusterName);
            clientOptions.setEnv(env);
            clientOptions.setUserName(userName);
            return new ApolloClientProxy(apolloOpenApiClient, clientOptions);
        }
    }
}